package bancosys.tec.persist.validator;

import java.beans.PropertyDescriptor;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;

import javax.persistence.Column;

import org.apache.commons.beanutils.PropertyUtils;
import org.hibernate.Session;
import org.hibernate.metadata.ClassMetadata;
import org.hibernate.validator.NotNull;

import bancosys.tec.persist.Persistable;
import bancosys.tec.persist.PersistenceController;
import bancosys.tec.persist.authorization.annotation.DisplayName;
import bancosys.tec.persist.bussobj.PersistableBusinessObject;
import bancosys.tec.persist.dao.BaseDAO;
import bancosys.tec.persist.dao.BasicDataBeanNotFoundException;
import bancosys.tec.persist.dao.BeanNotFoundException;
import bancosys.tec.persist.hibernate.impl.HibernateDependencyMetaData;
import bancosys.tec.persist.hibernate.impl.HibernateEntityDependency;
import bancosys.tec.persist.persister.annotation.Unique;
import bancosys.tec.persist.span.SpanAwareBean;
import bancosys.tec.persist.span.SpanAwareDAO;
import bancosys.tec.persist.span.SpanAwarePersister;

/**
 * @created Feb 6, 2007
 * @author Gustavo Almeida
 * @deprecated Utilize validators registráveis
 */
@Deprecated
public class DefaultBeanValidator implements BeanValidator {

    private volatile Set<Class<?>> ignoreClasses = new HashSet<Class<?>>();

    private volatile Set<Class<?>> ignoreDependencies = new HashSet<Class<?>>();

    /**
     * Controller de persistência utilizado para abrir transações caso seja necessário.
     */
    private PersistenceController persistenceController;

    /**
     * Meta Dados contendo as dependências das classes mapeadas no hibernate.
     */
    private HibernateDependencyMetaData hibernateDependencyMetaData;

    /**
     * Define o <code>PersistenceController</code>.
     * 
     * @param persistenceController o <code>PersistenceController</code>.
     */
    public void setPersistenceController(PersistenceController persistenceController) {
        this.persistenceController = persistenceController;
    }

    /**
     * Devolve o <code>PersistenceController</code>.
     * 
     * @return o <code>PersistenceController</code>.
     */
    public PersistenceController getPersistenceController() {
        return this.persistenceController;
    }

    /**
     * Método de acesso para hibernateDependencyMetaData.
     * 
     * @return instância de {@link HibernateDependencyMetaData HibernateDependencyMetaData}.
     */
    public HibernateDependencyMetaData getHibernateDependencyMetaData() {
        return this.hibernateDependencyMetaData;
    }

    /**
     * Método de acesso para hibernateDependencyMetaData.
     * 
     * @param hibernateDependencyMetaData instância de {@link HibernateDependencyMetaData HibernateDependencyMetaData}.
     */
    public void setHibernateDependencyMetaData(final HibernateDependencyMetaData hibernateDependencyMetaData) {
        this.hibernateDependencyMetaData = hibernateDependencyMetaData;
    }

    /**
     * Valida insert.
     * 
     * @param bean bean.
     * @return erros ou null.
     */
    public List<ValidationError> validateInsert(final Persistable bean) {
        List<ValidationError> errors = null;

        errors = this.addError(errors, this.validateDuplicatedKey(bean));
        errors = this.sumErrors(errors, this.uniqueInsertValidation(bean));

        if (bean instanceof SpanAwareBean) {
            errors = this.addError(errors, this.validateSpanInsert((SpanAwareBean) bean));
        }
        return errors;
    }

    /**
     * Executa validacoes genéricas nos campos.
     * 
     * @param bean bean.
     * @return erros ou null
     */
    protected final List<ValidationError> validateFields(Persistable bean) {
        PropertyDescriptor[] properties = PropertyUtils.getPropertyDescriptors(bean);
        SortedMap<String, Method> propertyMap = new TreeMap<String, Method>();

        SortedMap<String, Method> propertySizeMap = new TreeMap<String, Method>();
        for (PropertyDescriptor property : properties) {
            final Method getter = PropertyUtils.getReadMethod(property);
            if (getter != null) {
                if (getter.isAnnotationPresent(NotNull.class)) {
                    String name =
                            getter.isAnnotationPresent(DisplayName.class) ? getter.getAnnotation(DisplayName.class).value() : getter
                                    .getName();
                    propertyMap.put(name, getter);
                }

                if (getter.isAnnotationPresent(Column.class)) {
                    String name =
                            getter.isAnnotationPresent(DisplayName.class) ? getter.getAnnotation(DisplayName.class).value() : getter
                                    .getName();
                    propertySizeMap.put(name, getter);
                }
            }
        }
        List<ValidationError> errors = this.validateRequiredFields(bean, propertyMap);
        return errors;
    }

    /**
     * Executa validacao de campos obrigatórios.
     * 
     * @param bean bean.
     * @param propertyMap Mapa com os métodos e campos a serem validados.
     * @return erros ou null
     */
    private List<ValidationError> validateRequiredFields(Persistable bean, SortedMap<String, Method> propertyMap) {
        List<ValidationError> errors = null;
        for (Map.Entry<String, Method> propertyKey : propertyMap.entrySet()) {
            try {
                if (propertyKey.getValue().invoke(bean) == null) {
                    errors =
                            this.addError(errors, new ValidationError("persist.validationError.requiredField", bean, propertyKey.getKey()));
                }
            } catch (IllegalAccessException e) {
                errors = this.addError(errors, new ValidationError(e.getMessage(), bean));
            } catch (InvocationTargetException e) {
                errors = this.addError(errors, new ValidationError(e.getMessage(), bean));
            }
        }
        return errors;
    }


    /**
     * Valida autorizacao do insert.
     * 
     * @param bean bean.
     * @return erros ou null.
     */
    public List<ValidationError> validateInsertAuthorization(final Persistable bean) {
        List<ValidationError> errors = null;
        errors = this.addError(errors, this.validateDuplicatedKey(bean));
        errors = this.sumErrors(errors, this.uniqueInsertValidation(bean));
        if (bean instanceof SpanAwareBean) {
            errors = this.addError(errors, this.validateSpanInsert((SpanAwareBean) bean));
        }
        return errors;
    }

    /**
     * Valida a inserção de beans com vigência.
     * 
     * @param span bean.
     * @return erro de validação ou <code>null</code>.
     */
    @SuppressWarnings("unchecked")
    protected ValidationError validateSpanInsert(final SpanAwareBean span) {
        final SpanAwareDAO dao = (SpanAwareDAO) span.getController().getDAOFactory().getGenericDAO(span.getClass());
        try {
            final SpanAwarePersister persister = (SpanAwarePersister) span.getPersister();
            final Map<String, Object> spanKeys = persister.getSpanKeys(span);
            final SpanAwareBean lastSpan = dao.findLastSpan(spanKeys);
            if (!lastSpan.getPk().equals(span.getPk()) && !lastSpan.getDataInicio().before(span.getDataInicio())) {
                return new ValidationError("persist.validationError.dateBeforeLastRecorded", span);
            }
        } catch (final BeanNotFoundException e) {
            // do nothing;
        }
        return null;
    }

    /**
     * Valida remove.
     * 
     * @param bean bean.
     * @return erros ou null.
     */
    public List<ValidationError> validateRemove(final Persistable bean) {
        List<ValidationError> errors = null;
        errors = this.sumErrors(errors, this.validateFK(bean));

        if (bean instanceof SpanAwareBean) {
            final SpanAwareBean span = (SpanAwareBean) bean;
            if (!span.getDataFim().equals(SpanAwareBean.DATA_FIM)) {
                errors = this.addError(errors, new ValidationError("persist.validationError.onlyLastTermCanBeRemoved", bean));
            }
        }

        return errors;
    }

    /**
     * Valida autorizacao do remove.
     * 
     * @param bean bean.
     * @return erros ou null.
     */
    public List<ValidationError> validateRemoveAuthorization(final Persistable bean) {
        List<ValidationError> errors = null;

        if (bean instanceof SpanAwareBean) {
            final SpanAwareBean span = (SpanAwareBean) bean;
            if (!span.getDataFim().equals(SpanAwareBean.DATA_FIM)) {
                errors = this.addError(errors, new ValidationError("persist.validationError.onlyLastTermCanBeRemoved", bean));
            }
        }

        return errors;
    }

    /**
     * Valida update.
     * 
     * @param bean bean.
     * @return erros ou null.
     */
    public List<ValidationError> validateUpdate(final Persistable bean) {
        List<ValidationError> errors = null;
        errors = this.sumErrors(errors, this.uniqueUpdateValidation(bean));

        if (bean instanceof SpanAwareBean) {
            final SpanAwareBean span = (SpanAwareBean) bean;
            if (!span.getDataFim().equals(SpanAwareBean.DATA_FIM)) {
                errors = this.addError(errors, new ValidationError("persist.validationError.onlyLastTermCanBeUpdated", bean));
            }
        }

        return errors;
    }

    /**
     * Valida autorizacao do update.
     * 
     * @param bean bean.
     * @return erros ou null.
     */
    public List<ValidationError> validateUpdateAuthorization(final Persistable bean) {
        List<ValidationError> errors = null;
        errors = this.sumErrors(errors, this.uniqueUpdateValidation(bean));

        if (bean instanceof SpanAwareBean) {
            final SpanAwareBean span = (SpanAwareBean) bean;
            if (!span.getDataFim().equals(SpanAwareBean.DATA_FIM)) {
                errors = this.addError(errors, new ValidationError("persist.validationError.onlyLastTermCanBeUpdated", bean));
            }
        }

        return errors;
    }

    /**
     * Adicionar um erro a lista de erros, se o erro for <code>null</code> não faz nada, <BR>
     * se a lista for <code>null</code> aloca a lista se necessario.
     * 
     * @param errors lista de erros
     * @param error erro
     * @return lista alterada
     */
    protected List<ValidationError> addError(final List<ValidationError> errors, final ValidationError error) {
        List<ValidationError> errorList = errors;
        if (error == null) {
            return errorList;
        }
        if (errorList == null) {
            errorList = new LinkedList<ValidationError>();
        }
        this.validateAddError(errorList, error);
        return errorList;
    }

    /**
     * Valida a duplicidade do bean no banco pela sua pk.
     * 
     * @param bean bean a ser validado.
     * @return erro de validação ou <code>null</code>.
     */
    protected ValidationError validateDuplicatedKey(final Persistable bean) {
        if (bean instanceof PersistableBusinessObject) {
            final PersistableBusinessObject bo = (PersistableBusinessObject) bean;
            final BaseDAO<PersistableBusinessObject> dao = bean.getController().getDAOFactory().getGenericDAO(bo.getClass());
            try {
                final Serializable pk = bo.getPk();
                if (pk != null) {
                    dao.findByPk(pk);
                    final ClassMetadata cm = bean.getPurePersister().getSessionFactory().getClassMetadata(bean.getClass());
                    return new ValidationError(cm.getIdentifierPropertyName(), "persist.validationError.dataAlreadyOnDatabase", bean);
                }
            } catch (final BeanNotFoundException e) {
                return null;
            } catch (final BasicDataBeanNotFoundException e) {
                return null;
            }
        }
        return null;
    }

    /**
     * Método que valida se o bean possui FK's que serão quebradas quando o bean for removido.
     * 
     * @param bean bean.
     * @return erro.
     */
    protected List<ValidationError> validateFK(final Persistable bean) {
        final List<ValidationError> errors = new ArrayList<ValidationError>();
        if (this.isIgnoreFK(bean.getClass())) {
            return errors;
        }
        final List<? extends HibernateEntityDependency> dependenciesFor =
                this.getHibernateDependencyMetaData().getDependenciesFor(bean.getClass().getName());
        final Session session = bean.getController().getSessionFactory().getCurrentSession();
        final Serializable pk = bean.getPk();

        // mapa com o HibernateEntityDependency e o número de dependências
        Map<HibernateEntityDependency, Number> dependenciesMap = new HashMap<HibernateEntityDependency, Number>();

        // verifica as dependências
        for (HibernateEntityDependency dependency : dependenciesFor) {
            // verifica os ignores
            if (this.checkIgnoreDependency(dependency.getTipoDependente())) {
                continue;
            }
            Number n = dependency.countDependencies(session, pk);
            if (n.longValue() > 0) {
                dependenciesMap.put(dependency, n);
            }
        }

        // remove as hierarquias
        this.filtraDependenciasFolhas(dependenciesMap.keySet());

        // adiciona as mensagens de erro
        for (Entry<HibernateEntityDependency, Number> entry : dependenciesMap.entrySet()) {
            HibernateEntityDependency dependency = entry.getKey();
            Number numeroDependencias = entry.getValue();

            String nomeDependencia = null;
            if (dependency.getTipoDependente().getAnnotation(DisplayName.class) != null) {
                DisplayName displayName = dependency.getTipoDependente().getAnnotation(DisplayName.class);
                nomeDependencia = displayName.value();
            } else {
                nomeDependencia = dependency.getDependente();
            }

            errors.add(new ValidationError(dependency.getDependente(), "persist.validationError.cantRemoveDueToDependencies", bean,
                    nomeDependencia, numeroDependencias));
        }

        return errors;
    }

    /**
     * Filtra a coleção original removendo as dependências que são pai de alguma outra dependência, ou seja, removendo as hierarquias,
     * deixando somente as folhas.
     * 
     * @param dependenciesFor set com as dependências
     */
    private void filtraDependenciasFolhas(Set<HibernateEntityDependency> dependenciesFor) {
        // Para cada dependência
        for (Iterator<HibernateEntityDependency> it = dependenciesFor.iterator(); it.hasNext();) {
            HibernateEntityDependency dependency = it.next();

            boolean ehPai = false;

            // verifica se a dependencia atual é pai de alguma outra dependência
            for (HibernateEntityDependency hibernateEntityDependency : dependenciesFor) {
                if (dependency.getTipoDependente().isAssignableFrom(hibernateEntityDependency.getTipoDependente())
                        && !dependency.getTipoDependente().equals(hibernateEntityDependency.getTipoDependente())) {
                    // se for pai de algum outro elemento interrompe o loop
                    ehPai = true;
                    break;
                }
            }

            // se é pai de algum outro elemento remove do set
            if (ehPai) {
                it.remove();
            }

        }
    }

    /**
     * Dadas duas listas, se uma delas for <code>null</code>, devolve a outra, se ambas forem <code>null</code> devolve <code>null</code>,
     * se ambas forem diferentes de <code>null</code> devolve uma terceira lista com o conteudo das duas.
     * 
     * @param errors lista 1
     * @param moreErrors lista 2
     * @return soma das duas listas ou <code>null</code>.
     */
    protected List<ValidationError> sumErrors(final List<ValidationError> errors, final List<ValidationError> moreErrors) {
        if ((errors == null) && (moreErrors == null)) {
            return null;
        }
        if ((errors != null) && (moreErrors == null)) {
            return errors;
        }
        if ((errors == null) && (moreErrors != null)) {
            return moreErrors;
        }
        this.validateAddError(errors, moreErrors);
        return errors;
    }

    /**
     * Valida se a mensagem de erro já esta na lista, para que não sejam incluídas mensagens duplicadas
     * 
     * @param errors - lista com as mensagens de erros já gravadas
     * @param error - nova mensagem de erro, para ser verificado
     * @return lista com as mensagens de erro
     */
    private List<ValidationError> validateAddError(final List<ValidationError> errors, final ValidationError error) {
        for (final ValidationError validationError : errors) {
            final String msgErro = validationError.getMessage();
            if (msgErro.equals(error.getMessage())) {
                return errors;
            }
        }
        errors.add(error);
        return errors;
    }

    /**
     * Adiciona uma Lista de Erros em outra. Validando se não estão sendo incluídos erros duplicados
     * 
     * @param errors - Lista principal, os erros da outra lista serão incluídos nesta.
     * @param moreErrors - Lista que vai ser adicionada na outra lista.
     * @return Lista com todos os erros não duplicados.
     */
    private List<ValidationError> validateAddError(final List<ValidationError> errors, final List<ValidationError> moreErrors) {
        for (final ValidationError error : moreErrors) {
            this.validateAddError(errors, error);
        }
        return errors;
    }

    /**
     * Adiciona uma dependência a ser ignorada na validação de FK. Para especificar fk's a serem ignorados estenda essa classe e chame esse
     * método do construtor.
     * 
     * @param dependecy classe da dependência.
     */
    protected final void addIgnoreFKDependency(final Class<? extends Persistable> dependecy) {
        this.addIgnoreClass(dependecy);
    }

    /**
     * Validação de unicidade na inserção.
     * 
     * @param bean dado a ser validado.
     * @return lista com os erros de validação ou <code>null</code> caso não haja erros.
     */
    protected List<ValidationError> uniqueInsertValidation(final Persistable bean) {
        List<ValidationError> errors = null;
        for (final UniqueValidation validation : this.uniqueValidations(bean)) {
            errors = this.addError(errors, validation.validateInsert());
        }
        return errors;
    }

    /**
     * Validação de unicidade na modificação.
     * 
     * @param bean dado a ser validado.
     * @return lista com os erros de validação ou <code>null</code> caso não haja erros.
     */
    protected List<ValidationError> uniqueUpdateValidation(final Persistable bean) {
        List<ValidationError> errors = null;
        for (final UniqueValidation validation : this.uniqueValidations(bean)) {
            errors = this.addError(errors, validation.validateUpdate());
        }
        return errors;
    }

    /**
     * Dado um bean devolve todas as validações de unicidade que devem ser executadas para ele.
     * 
     * @param bean bean.
     * @return validações de unicidade.
     */
    protected Collection<UniqueValidation> uniqueValidations(final Persistable bean) {
        final SortedMap<String, UniqueValidation> validations = new TreeMap<String, UniqueValidation>();
        final PropertyDescriptor[] properties = PropertyUtils.getPropertyDescriptors(bean);
        for (final PropertyDescriptor property : properties) {
            final Method getter = PropertyUtils.getReadMethod(property);
            if (getter != null) {
                final Unique unique = getter.getAnnotation(Unique.class);
                if (unique != null) {
                    if ("".equals(unique.value())) {
                        final String value = this.getClass().getName() + property;
                        validations.put(value, new UniqueValidation(property.getName(), getter, bean));
                    } else {
                        final String key = unique.value();
                        UniqueValidation validation = validations.get(key);
                        if (validation == null) {
                            validation = new UniqueValidation(property.getName(), getter, bean);
                            validations.put(key, validation);
                        } else {
                            validation.addProperty(property.getName(), getter);
                        }
                    }
                }
            }
        }
        return validations.values();
    }

    /**
     * Verifica se as fks para o tipo passada devem ser verificadas.
     * 
     * @param type o tipo
     * @return boolean
     */
    private boolean isIgnoreFK(final Class<?> type) {
        final Set<Class<?>> types = this.ignoreClasses;
        if ((types == null) || (Object.class == type)) {
            return false;
        }
        if (types.contains(type)) {
            return true;
        }
        final boolean ignoreFK = this.checkIgnoreRecurse(type.getSuperclass(), types);
        if (ignoreFK && !types.contains(type)) {
            this.addIgnoreClass(type);
        }
        return ignoreFK;
    }

    /**
     * @param type o tipo a ser verificado
     * @param types o conjunto de tipos que devem ser ignorados
     * @return boolean
     */
    private boolean checkIgnoreRecurse(final Class<?> type, final Set<Class<?>> types) {
        if (Object.class == type) {
            return false;
        }
        if (types.contains(type)) {
            return true;
        }
        final boolean ignore = this.checkIgnoreRecurse(type.getSuperclass(), types);
        return ignore;
    }

    /**
     * Ignora todas as dependencias para esta classe
     * 
     * @param type {@link Class}
     */
    protected final void addIgnoreClass(final Class<?> type) {
        final Set<Class<?>> types = this.ignoreClasses;
        final Set<Class<?>> modded = new HashSet<Class<?>>(types);
        if (modded.add(type)) {
            this.ignoreClasses = modded;
        }
    }

    /**
     * Faz com que dependencias do tipo passado para o tipo sendo apagado sejam ignoradas. <BR>
     * Em outras palavras, ao apagar uma entidade T, se uma entidade aqui adicionada ainda <br>
     * tiver referência a esta, esta referência nao será verificada. <br>
     * 
     * @param type o tipo
     */
    protected final synchronized void addIgnoreDependency(final Class<?> type) {
        final Set<Class<?>> set = this.ignoreDependencies;
        final Set<Class<?>> modded = new HashSet<Class<?>>(set);
        if (modded.add(type)) {
            this.ignoreDependencies = modded;
        }
    }

    /**
     * @param type o tipo
     * @return boolean
     */
    protected final boolean checkIgnoreDependency(final Class<?> type) {
        final Set<Class<?>> deps = this.ignoreDependencies;
        if ((type == Object.class) || (type == null)) {
            return false;
        }
        if (deps.contains(type)) {
            return true;
        }
        final boolean checkIgnoreRecurse = this.checkIgnoreRecurse(type.getSuperclass(), deps);
        if (checkIgnoreRecurse) {
            this.addIgnoreDependency(type);
        }
        return checkIgnoreRecurse;
    }

    /**
     * Verifica se o valor passado é maior que zero.
     * 
     * @param value Valor
     * @param bean Entidade que possui o valor a ser verificado.
     * @param fieldName Nome do campo a ser exibido na mensagem caso o valor não seje maior que zero.
     * @return {@link ValidationError} ou <code>null</code> Se o valor for maior que zero.
     */
    protected final ValidationError checkValueGreaterThanZero(BigDecimal value, Persistable bean, String fieldName) {
        if (value.compareTo(BigDecimal.ZERO) <= 0) {
            return new ValidationError("persist.validationError.valueGreaterThanZero", bean, fieldName);
        }
        return null;
    }

    /**
     * Verifica se o valor passado é menor que zero.
     * 
     * @param value Valor
     * @param bean Entidade que possui o valor a ser verificado.
     * @param fieldName Nome do campo a ser exibido na mensagem caso o valor não seje menor que zero.
     * @return {@link ValidationError} ou <code>null</code> Se o valor for menor que zero.
     */
    protected final ValidationError checkValueLessThanZero(BigDecimal value, Persistable bean, String fieldName) {
        if (value.compareTo(BigDecimal.ZERO) >= 0) {
            return new ValidationError("persist.validationError.valueLessThanZero", bean, fieldName);
        }
        return null;
    }

    /**
     * Verifica se o valor passado é maior ou igual a zero.
     * 
     * @param value Valor
     * @param bean Entidade que possui o valor a ser verificado.
     * @param fieldName Nome do campo a ser exibido na mensagem caso o valor não seje maior ou igual a zero.
     * @return {@link ValidationError} ou <code>null</code> Se o valor for maior ou igual a zero.
     */
    protected final ValidationError checkValueGreaterEqualsZero(BigDecimal value, Persistable bean, String fieldName) {
        if (value.compareTo(BigDecimal.ZERO) < 0) {
            return new ValidationError("persist.validationError.valueGreaterEqualsZero", bean, fieldName);
        }
        return null;
    }

    /**
     * Verifica se o valor passado é menor que zero.
     * 
     * @param value Valor
     * @param bean Entidade que possui o valor a ser verificado.
     * @param fieldName Nome do campo a ser exibido na mensagem caso o valor não seje menor ou igual a zero.
     * @return {@link ValidationError} ou <code>null</code> Se o valor for menor ou igual a zero.
     */
    protected final ValidationError checkValueLessEqualsZero(BigDecimal value, Persistable bean, String fieldName) {
        if (value.compareTo(BigDecimal.ZERO) > 0) {
            return new ValidationError("persist.validationError.valueLessEqualsZero", bean, fieldName);
        }
        return null;
    }

    /**
     * @param ignoreClasses the ignoreClasses to set
     */
    public void setIgnoreClasses(final Set<Class<?>> ignoreClasses) {
        this.ignoreClasses = new HashSet<Class<?>>(ignoreClasses);
    }

    /**
     * {@inheritDoc}
     */
    public final Class<? extends Persistable> getTargetClass() {
        throw new IllegalStateException();
    }
}
